大部分复制整理于以下四篇文章,侵删致歉。
Generator函数的含义与用法
Thunk函数的含义与用法
co函数库的含义与用法
async函数的含义与用法
协程coroutine
多个线程互相协作,完成异步任务。
- 协程A开始执行。
- 协程A执行到一半,进入暂停,执行权转移到协程B。
- (一段时间后)协程B交还执行权。
- 协程A恢复执行。
1 | function asnycJob() { |
上面代码的函数 asyncJob
是一个协程。yield
命令表示执行到此处,执行权将交给其他协程。也就是说,yield
命令是异步两个阶段的分界线。
协程遇到 yield
命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。
Generator函数
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
1 | function* gen(x){ |
Generator 函数不同于普通函数,是可以暂停执行的,所以函数名之前要加星号,以示区别。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield
语句注明。
调用 Generator 函数,会返回一个内部指针(即遍历器 )g
。
调用指针 g
的 next
方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的 yield
语句,上例是执行到 x + 2
为止。
换言之,next
方法的作用是分阶段执行 Generator 函数。每次调用 next
方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性
)。
value
属性是 yield
语句后面表达式的值,表示当前阶段的值;done
属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
数据交换
1 | function* gen(x){ |
第一个 next
方法的 value 属性,返回表达式 x + 2
的值(3)
。
第二个 next
方法带有参数2
,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果(即yield
语句后面表达式的值),被函数体内的变量 y
接收。因此,这一步的 value 属性,返回的就是2(变量 y 的值)
。
错误处理
捕获函数体外错误
1 | function* gen(x){ |
上面代码的最后一行,Generator 函数体外,使用指针对象的 throw
方法抛出的错误,可以被函数体内的 try ... catch
代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
实例
Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。
1 | var fetch = require('node-fetch'); |
Thunk
1. 传值策略
1 |
|
“传值调用”(call by value): f(x + 5)即f(6)
“传名调用”(call by name): f(x + 5)即(x + 5) * 2
2. Thunk函数
Thunk 函数是传名调用
的一种实现策略,用来替换某个表达式。
1 | function f(m){ |
3. Thunk 函数(JavaScript)
在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
1 | // 正常版本的readFile(多参数版本) |
Thunk 函数转换器
1 | var Thunk = function(fn){ |
1 | // fs.readFile 的 Thunk 函数 |
4. Array.prototype.slice.call(arguments)
arrayObj.slice(start, [end])
:
截取数组的一部分call([thisObj[,arg1[arg2[[argN]]]]])
:
thisObj是一个对象的方法
arrg1~argN是参数
所以该方法用来把调用方法的参数截取出来。
1 | function test(a,b,c,d) |
因为arguments并不是真正的数组对象而是Object,只是与数组类似而已,所以它并没有slice
这个方法,而Array.prototype.slice.call(arguments, 1)
可以理解成是让arguments转换成一个数组对象,让arguments具有slice()
方法。要是直接写arguments.slice(1)
会报错。
原理
`Array.prototype.slice.call(arguments)能将具有length属性的对象转成数组,除了IE下的节点集合(因为ie下的dom对象是以com对象的形式实现的,js对象与com对象不能进行转换)
1 | var a={length:2,0:'first',1:'second'}; |
将函数的实际参数转换成数组的方法
var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments, 0);
1
2
3
4var args = [];
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
}
5. Thunkify
https://github.com/tj/node-thunkify
Turn a regular node function into one which returns a thunk, useful for generator-based flow control such as co.
1 | var thunkify = require('thunkify'); |
6. 基于 Thunk 函数的 Generator 执行器
1 | function run(fn) { |
上面代码的 run
函数,就是一个 Generator 函数的自动执行器。
内部的 next
函数就是 Thunk 的回调函数。next
函数先将指针移到 Generator 函数的下一步(gen.next 方法
),然后判断 Generator 函数是否结束(result.done 属性
),如果没结束,就将 next
函数再传入 Thunk 函数(result.value 属性
),否则就直接退出。
函数gen
封装了 n 个异步的读取文件操作,只要执行 run
函数,这些操作就会自动完成。
CO
The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc)
1 | //Generator 函数 |
例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38//只使用回调函数
var fs = require('fs');
function readFile(path, cb) {
fs.readFile(path, {encoding: 'utf8'}, cb);
}
readFile('a.js', function (err, dataA) {
console.log(dataA);
readFile('b.js', function (err, dataB) {
console.log(dataB);
readFile('c.js', function (err, dataC) {
console.log(dataC);
...
});
});
});
----------
//使用CO
var fs = require('fs');
var co = require('co');
function readFile(path) {
return function (cb) {
fs.readFile(path, {encoding: 'utf8'}, cb);
};
}
co(function* () {
var dataA = yield readFile('a.js');
console.log(dataA);
var dataB = yield readFile('b.js');
console.log(dataB);
var dataC = yield readFile('c.js');
console.log(dataC);
}).catch(function (err) {
console.log(err);
});
co 将所有 yield
后面的表达式都封装成了 Promise 对象(本身也返回一个Promise 对象),只有当前表达式执行结束后(即调用 .then
),然后会在 onFulfilled
函数内执行 gen.next(res)
将 res 赋值给 yield
左侧的变量并执行到下一个 yield
,下一个表达式执行结束后又调用 gen.next()
,如此循环,直至 done 变为 true。
ES6 中的 yield
后面可以跟任意类型的值,但 co 对此做了限制,只允许 yield
后跟 thunk
, promise
, generator
, generatorFunction
,array
或者 object
。
原理
co 函数库其实就是将两种自动执行器(Thunk 函数
和 Promise 对象
),包装成一个库。
- 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
- Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
使用 co 的前提条件是,Generator 函数的 yield
命令后面,只能是 Thunk
函数或 Promise
对象。
源码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38//co 函数接受 Generator 函数作为参数,返回一个 Promise 对象
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
//co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象
if (typeof gen === 'function') gen = gen.call(ctx);
//如果不是就返回,并将 Promise 对象的状态改为 resolved
if (!gen || typeof gen.next !== 'function') return resolve(gen);
//co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulefilled 函数。这主要是为了能够捕捉抛出的错误。
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
function next(ret) {
//检查当前是否为 Generator 函数的最后一步,如果是就返回
if (ret.done) return resolve(ret.value);
//确保每一步的返回值,是 Promise 对象
var value = toPromise.call(ctx, ret.value);
//使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
//在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
并发
把并发的操作都放在数组或对象里面, co允许某些操作同时进行,等到它们全部完成,才进行下一步。
1 | // 数组的写法 |
async
async 函数就是 Generator 函数的语法糖
1 | var fs = require('fs'); |
async 函数就是将 Generator 函数的星号(*
)替换成 async
,将 yield
替换成 await
。
优点
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
var result = asyncReadFile();
(2)更好的语义。
async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
实现
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。
1 | async function fn(args){ |
使用
同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then
方法添加回调函数。
当函数执行的时候,一旦遇到 await
就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
1 | function timeout(ms) { |
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await
命令放在 try...catch
代码块中。
1 | async function myFunction() { |
await 命令只能用在 async 函数之中。
可以使用 Promise.all 方法使多个请求并发执行。
1 | async function dbFuc(db) { |